/*-------------------------------------------------------------------------
 *
 * subtrans.c
 *		PostgreSQL 子事务日志管理器
 *
 * pg_subtrans 管理器是类似于 pg_xact 的管理器，它为每个事务存储父事务 ID。
 * 它是嵌套事务实现的基本部分。一个主事务的父事务为 InvalidTransactionId，而
 * 每个子事务都有其直接父事务。可以很容易地从子事务向父事务遍历，但反向
 * 遍历则不行。
 *
 * 这段代码基于 xact.c，但其稳健性要求与 pg_xact 完全不同，因为我们只需要
 * 记住当前打开的事务的 pg_subtrans 信息。因此，没有必要在崩溃和重启之间
 * 保留数据。
 *
 * 由于我们不关心在崩溃之间保留数据，因此没有 XLOG 交互。在数据库启动时，
 * 我们仅仅将 SUBTRANS 当前活动页面强制设置为零。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/backend/access/transam/subtrans.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/slru.h"
#include "access/subtrans.h"
#include "access/transam.h"
#include "pg_trace.h"
#include "utils/snapmgr.h"


/*
 * 定义了SubTrans页面的大小。一个页面的大小与Postgres中
 * 其他地方使用的BLCKSZ相同。
 *
 * 注意：由于TransactionIds为32位，并在0xFFFFFFFF处环绕，
 * SubTrans页面编号也在0xFFFFFFFF/SUBTRANS_XACTS_PER_PAGE处环绕，
 * 片段编号在0xFFFFFFFF/SUBTRANS_XACTS_PER_PAGE/SLRU_PAGES_PER_SEGMENT处分环绕。
 * 我们在这个模块中不需要显式注意这一事实，除非在TruncateSUBTRANS
 * 中比较片段和页面编号（见SubTransPagePrecedes）以及在StartupSUBTRANS中
 * 将它们置零。
 */

/* 每个xact需要四个字节 */
#define SUBTRANS_XACTS_PER_PAGE (BLCKSZ / sizeof(TransactionId))

#define TransactionIdToPage(xid) ((xid) / (TransactionId) SUBTRANS_XACTS_PER_PAGE)
#define TransactionIdToEntry(xid) ((xid) % (TransactionId) SUBTRANS_XACTS_PER_PAGE)


/*
 * 链接到用于SUBTRANS控制的共享内存数据结构
 */
static SlruCtlData SubTransCtlData;

#define SubTransCtl  (&SubTransCtlData)


static int	fc_ZeroSUBTRANSPage(int fc_pageno);
static bool fc_SubTransPagePrecedes(int fc_page1, int fc_page2);


/*
 * 在subtrans日志中记录子事务的父事务。
 */
void SubTransSetParent(TransactionId fc_xid, TransactionId fc_parent)
{
	int			fc_pageno = TransactionIdToPage(fc_xid);
	int			fc_entryno = TransactionIdToEntry(fc_xid);
	int			fc_slotno;
	TransactionId *fc_ptr;

	Assert(TransactionIdIsValid(fc_parent));
	Assert(TransactionIdFollows(fc_xid, fc_parent));

	LWLockAcquire(SubtransSLRULock, LW_EXCLUSIVE);

	fc_slotno = SimpleLruReadPage(SubTransCtl, fc_pageno, true, fc_xid);
	fc_ptr = (TransactionId *) SubTransCtl->shared->page_buffer[fc_slotno];
	fc_ptr += fc_entryno;

	/*
	 * 我们可能会尝试多次设置父xid，但我们
	 * 不应该将xid从一个有效xid更改为另一个有效
	 * xid，这将损坏数据结构。
	 */
	if (*fc_ptr != fc_parent)
	{
		Assert(*fc_ptr == InvalidTransactionId);
		*fc_ptr = fc_parent;
		SubTransCtl->shared->page_dirty[fc_slotno] = true;
	}

	LWLockRelease(SubtransSLRULock);
}

/*
 * 查询subtrans日志中事务的父事务。
 */
TransactionId SubTransGetParent(TransactionId fc_xid)
{
	int			fc_pageno = TransactionIdToPage(fc_xid);
	int			fc_entryno = TransactionIdToEntry(fc_xid);
	int			fc_slotno;
	TransactionId *fc_ptr;
	TransactionId fc_parent;

	/* 不能询问可能不再存在的东西 */
	Assert(TransactionIdFollowsOrEquals(fc_xid, TransactionXmin));

	/* Bootstrap和冻结的XIDs没有父事务 */
	if (!TransactionIdIsNormal(fc_xid))
		return InvalidTransactionId;

	/* 锁由 SimpleLruReadPage_ReadOnly 获取 */

	fc_slotno = SimpleLruReadPage_ReadOnly(SubTransCtl, fc_pageno, fc_xid);
	fc_ptr = (TransactionId *) SubTransCtl->shared->page_buffer[fc_slotno];
	fc_ptr += fc_entryno;

	fc_parent = *fc_ptr;

	LWLockRelease(SubtransSLRULock);

	return fc_parent;
}

/*
 * SubTransGetTopmostTransaction
 *
 * 返回给定事务ID的最上层事务。
 *
 * 因为我们无法回溯到TransactionXmin，因此这个函数可能会
 * 撒谎并返回一个中间子事务ID而不是真实的最上层父ID。
 * 这没关系，因为实际上我们只关心检测最上层父事务
 * 是否仍在运行，或者它是否是当前快照仍在运行的事务列表的一部分。
 * 因此，任何在TransactionXmin之前的XID都与其他任何XID一样好。
 */
TransactionId SubTransGetTopmostTransaction(TransactionId fc_xid)
{
	TransactionId fc_parentXid = fc_xid,
				fc_previousXid = fc_xid;

	/* 不能询问可能不再存在的东西 */
	Assert(TransactionIdFollowsOrEquals(fc_xid, TransactionXmin));

	while (TransactionIdIsValid(fc_parentXid))
	{
		fc_previousXid = fc_parentXid;
		if (TransactionIdPrecedes(fc_parentXid, TransactionXmin))
			break;
		fc_parentXid = SubTransGetParent(fc_parentXid);

		/*
		 * 按惯例，父xid首先被分配，因此应该始终
		 * 在子xid之前。其他任何情况都指向损坏的数据
		 * 结构，可能导致无限循环，因此退出。
		 */
		if (!TransactionIdPrecedes(fc_parentXid, fc_previousXid))
			elog(ERROR, "pg_subtrans contains invalid entry: xid %u points to parent xid %u",
				 fc_previousXid, fc_parentXid);
	}

	Assert(TransactionIdIsValid(fc_previousXid));

	return fc_previousXid;
}


/*
 * SUBTRANS的共享内存初始化
 */
Size SUBTRANSShmemSize(void)
{
	return SimpleLruShmemSize(NUM_SUBTRANS_BUFFERS, 0);
}

void SUBTRANSShmemInit(void)
{
	SubTransCtl->PagePrecedes = fc_SubTransPagePrecedes;
	SimpleLruInit(SubTransCtl, "Subtrans", NUM_SUBTRANS_BUFFERS, 0,
				  SubtransSLRULock, "pg_subtrans",
				  LWTRANCHE_SUBTRANS_BUFFER, SYNC_HANDLER_NONE);
	SlruPagePrecedesUnitTests(SubTransCtl, SUBTRANS_XACTS_PER_PAGE);
}

/*
 * 此函数必须在系统安装时调用一次。它创建
 * 初始的SUBTRANS段。（假设SUBTRANS目录
 * 已由initdb shell脚本创建，并且已经调用了SUBTRANSShmemInit。）
 *
 * 注意：现在实际上并不需要创建初始段，
 * 因为slru.c会在第一次写入时创建它。但是我们
 * 还是可以这样做，以确保目录正确设置。
 */
void BootStrapSUBTRANS(void)
{
	int			fc_slotno;

	LWLockAcquire(SubtransSLRULock, LW_EXCLUSIVE);

	/* 创建并将subtrans日志的第一页置零 */
	fc_slotno = fc_ZeroSUBTRANSPage(0);

	/* 确保它被写出 */
	SimpleLruWritePage(SubTransCtl, fc_slotno);
	Assert(!SubTransCtl->shared->page_dirty[fc_slotno]);

	LWLockRelease(SubtransSLRULock);
}

/*
 * 将SUBTRANS的一个页面初始化（或重新初始化）为零。
 *
 * 该页面实际上并未写入，只是在共享内存中设置。
 * 新页面的槽号将被返回。
 *
 * 入口时必须持有控制锁，并将在退出时保持。
 */
static int fc_ZeroSUBTRANSPage(int fc_pageno)
{
	return SimpleLruZeroPage(SubTransCtl, fc_pageno);
}

/*
 * 这必须在postmaster或standalone-backend启动期间调用一次，
 * 在StartupXLOG初始化了ShmemVariableCache->nextXid之后。
 *
 * oldestActiveXID是任何准备好的事务的最旧XID，或者如果没有则为nextXid。
 */
void StartupSUBTRANS(TransactionId fc_oldestActiveXID)
{
	FullTransactionId fc_nextXid;
	int			fc_startPage;
	int			fc_endPage;

	/*
	 * 由于我们不期望pg_subtrans在崩溃之间是有效的，
	 * 我们在启动期间将当前活动页面初始化为零。
	 * 每当我们进入新的页面时，ExtendSUBTRANS同样会将新页面清零，
	 * 而不考虑以前在磁盘上存在的内容。
	 */
	LWLockAcquire(SubtransSLRULock, LW_EXCLUSIVE);

	fc_startPage = TransactionIdToPage(fc_oldestActiveXID);
	fc_nextXid = ShmemVariableCache->nextXid;
	fc_endPage = TransactionIdToPage(XidFromFullTransactionId(fc_nextXid));

	while (fc_startPage != fc_endPage)
	{
		(void) fc_ZeroSUBTRANSPage(fc_startPage);
		fc_startPage++;
		/* 必须考虑环绕 */
		if (fc_startPage > TransactionIdToPage(MaxTransactionId))
			fc_startPage = 0;
	}
	(void) fc_ZeroSUBTRANSPage(fc_startPage);

	LWLockRelease(SubtransSLRULock);
}

/*
 * 执行检查点 --- 无论是在关闭期间还是动态执行
 */
void CheckPointSUBTRANS(void)
{
	/*
	 * 将脏的SUBTRANS页面写入磁盘
	 *
	 * 从正确性的角度来看，这实际上并不是必要的。我们这样做只是为了
	 * 提高脏页面的写入由检查点进程而不是后端完成的可能性。
	 */
	TRACE_POSTGRESQL_SUBTRANS_CHECKPOINT_START(true);
	SimpleLruWriteAll(SubTransCtl, true);
	TRACE_POSTGRESQL_SUBTRANS_CHECKPOINT_DONE(true);
}


/*
 * 确保SUBTRANS有空间用于新分配的XID。
 *
 * 注意：在持有XidGenLock时调用。我们希望它大多数时间非常快；
 * 即使它不是那么快，除非我们被迫写出一个脏的subtrans页面以便在
 * 共享内存中腾出空间，否则实际上不需要发生I/O。
 */
void ExtendSUBTRANS(TransactionId fc_newestXact)
{
	int			fc_pageno;

	/*
	 * 除了页面的第一个 XID，没什么工作要做。但要注意：
	 * 刚刚环绕过后，页面零的第一个 XID 是
	 * FirstNormalTransactionId。
	 */
	if (TransactionIdToEntry(fc_newestXact) != 0 &&
		!TransactionIdEquals(fc_newestXact, FirstNormalTransactionId))
		return;

	fc_pageno = TransactionIdToPage(fc_newestXact);

	LWLockAcquire(SubtransSLRULock, LW_EXCLUSIVE);

	/* 将页面清零 */
	fc_ZeroSUBTRANSPage(fc_pageno);

	LWLockRelease(SubtransSLRULock);
}


/*
 * 删除在持有传入事务ID的段之前的所有SUBTRANS段
 *
 * oldestXact是任何运行事务的最旧TransactionXmin。此操作
 * 仅在检查点期间调用。
 */
void TruncateSUBTRANS(TransactionId fc_oldestXact)
{
	int			fc_cutoffPage;

	/*
	 * 截止点是包含oldestXact的段的开始。我们将包含
	 * oldestXact的*页面*传递给SimpleLruTruncate。我们向后退
	 * 一个事务以避免在rare情况下传递尚未创建的截止页面，
	 * 该情况下oldestXact可能是页面上的第一个项目，并且
	 * oldestXact == next XID。在这种情况下，如果不减去
	 * 一，我们将触发SimpleLruTruncate的环绕检测。
	 */
	TransactionIdRetreat(fc_oldestXact);
	fc_cutoffPage = TransactionIdToPage(fc_oldestXact);

	SimpleLruTruncate(SubTransCtl, fc_cutoffPage);
}


/*
 * 决定一个SUBTRANS页面编号是否在截断目的上是“更旧的”。
 * 类似于CLOGPagePrecedes()。
 */
static bool fc_SubTransPagePrecedes(int fc_page1, int fc_page2)
{
	TransactionId fc_xid1;
	TransactionId fc_xid2;

	fc_xid1 = ((TransactionId) fc_page1) * SUBTRANS_XACTS_PER_PAGE;
	fc_xid1 += FirstNormalTransactionId + 1;
	fc_xid2 = ((TransactionId) fc_page2) * SUBTRANS_XACTS_PER_PAGE;
	fc_xid2 += FirstNormalTransactionId + 1;

	return (TransactionIdPrecedes(fc_xid1, fc_xid2) &&
			TransactionIdPrecedes(fc_xid1, fc_xid2 + SUBTRANS_XACTS_PER_PAGE - 1));
}
